﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;


namespace ParticleSystem
{
    struct ParticleVertex
    {
        public Vector3 startPosition;
        public Vector2 uv;
        public Vector3 direction;
        public float speed;
        public float birthTime;
      

        public ParticleVertex(Vector3 StartPosition, Vector2 UV, Vector3 Direction, float Speed, float BirthTime)
        {
            this.startPosition = StartPosition;
            this.uv = UV;
            this.direction = Direction;
            this.speed = Speed;
            this.birthTime = BirthTime;
            
        }

        public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration
            (new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
             new VertexElement(12, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0),
             new VertexElement(20, VertexElementFormat.Vector3, VertexElementUsage.TextureCoordinate, 1),
             new VertexElement(32, VertexElementFormat.Single, VertexElementUsage.TextureCoordinate, 2),
             new VertexElement(36, VertexElementFormat.Single, VertexElementUsage.TextureCoordinate, 3));
    }



    public class ParticleSystem
    {
        VertexBuffer verts;
        IndexBuffer ints;
        GraphicsDevice graphicsDevice;
        Effect effect;

        Random rand;

        int nParticles;
        Vector2 particleSize;
        float maxTime;
        float area;
        Vector3 wind;
        Texture2D texture;
        float fadeInTime;
        float spreadAngle;
        float maxSpeed;

        ParticleVertex[] particles;
        int[] indices;

        //Queue values
        int activeStart = 0;
        int nActive = 0;







        public ParticleSystem(GraphicsDevice graphicsDevice, Texture2D tex, Effect effect, int nParticles,
                               Vector2 particleSize, float maxTime, Vector3 wind, float FadeInTime, float area, float spreadAngle, float maxSpeed)
        {
            this.nParticles = nParticles;
            this.particleSize = particleSize;
            this.maxTime = maxTime;
            this.graphicsDevice = graphicsDevice;
            this.wind = wind;
            this.texture = tex;
            this.effect = effect;
            this.fadeInTime = FadeInTime;
            this.area = area;
            this.spreadAngle = spreadAngle;
            this.maxSpeed = maxSpeed;
            this.rand = new Random();

            //create vertex and index buffer to hold all particles
            verts = new VertexBuffer(graphicsDevice, ParticleVertex.VertexDeclaration, nParticles * 4, BufferUsage.WriteOnly);
            ints = new IndexBuffer(graphicsDevice, typeof(int), nParticles * 6, BufferUsage.WriteOnly);

            GenerateParticles();


           
        }


        private void GenerateParticles()
        {
            //create vertex and index array for the nparticles
            particles = new ParticleVertex[nParticles * 4];
            indices = new int[nParticles * 6];

            Vector3 z = Vector3.Zero;

            int x = 0;
            for (int i = 0; i < nParticles * 4; i += 4)
            {
                particles[i + 0] = new ParticleVertex(z, new Vector2(0, 0), z, 0, -maxTime);
                particles[i + 1] = new ParticleVertex(z, new Vector2(0, 1), z, 0, -maxTime);
                particles[i + 2] = new ParticleVertex(z, new Vector2(1, 1), z, 0, -maxTime);
                particles[i + 3] = new ParticleVertex(z, new Vector2(1, 0), z, 0, -maxTime);

                indices[x++] = i + 0;
                indices[x++] = i + 3;
                indices[x++] = i + 2;
                indices[x++] = i + 2;
                indices[x++] = i + 1;
                indices[x++] = i + 0;
            }
        }


        //Set a particle to active and give it the given settings
        public void AddParticle(GameTime gameTime, Vector3 position)
        {

            //if no particles are available
            if (nActive + 4 == nParticles * 4)
                return;




            //generate direction within 15 degrees of (0,1,0)
            Vector3 offset = new Vector3(MathHelper.ToRadians(spreadAngle));
            Vector3 randAngle = Vector3.Backward + RandVec(-offset, offset);

            //generate position between (-400, 0, -400) and (400,0,400)
            Vector3 randPosition = RandVec(position + new Vector3(-area, 0, -area), position + new Vector3(area, 0, area));

            //generate speed between maxSpeed/2 and maxSpeed
            float randSpeed = (float)rand.NextDouble() * (maxSpeed / 2.0f) + maxSpeed;



            //at wich index this particle should be created

            int index = OffsetIndex(activeStart, nActive);
            nActive += 4;
            

            //determin the start time
            float birthTime = (float)(gameTime.TotalGameTime.TotalMilliseconds);

            //set the given parametres to the particle
            for (int i = 0; i < 4; i++)
            {
                particles[index + i].startPosition = randPosition;
                particles[index + i].direction = randAngle;
                particles[index + i].speed = randSpeed;
                particles[index + i].birthTime = birthTime;
            }
        }


        //increase start parameter by count positions. circle around the queue of the particle array
        private int OffsetIndex(int start, int count)
        {
            for (int i = 0; i < count; i++)
            {
                start++;

                if (start == particles.Length)
                {
                    start = 0;
                }
            }
            return start;
        }


        public void Update(GameTime gameTime, Vector3 position)
        {

            

            AddParticle(gameTime, position);

            float now = (float)gameTime.TotalGameTime.TotalMilliseconds;

            int startIndex = activeStart;
            //int end = OffsetIndex(activeStart, nActive);

            //for each active particle...
            for (int i = 0; i < nActive; i++)
            {
                if (startIndex == particles.Length)
                    startIndex = 0;
                //if this particle is older than its lifespan
                if (now - particles[startIndex].birthTime > maxTime)
                {
                    particles[startIndex].birthTime = -maxTime;
                    //advance active particles startposition past the particle`s index and reduce number of active particles
                    activeStart++;
                    nActive--;

                    if (activeStart == particles.Length)
                        activeStart = 0;

                    
                }
                startIndex++;
            }
            //update vertex and index buffers
            verts.SetData<ParticleVertex>(particles);
            ints.SetData<int>(indices);





        }


        public void Draw(Matrix View, Matrix Projection, Vector3 Up, Vector3 Right, GameTime gameTime)
        {
            //set vertex and index buffers
            graphicsDevice.SetVertexBuffer(verts);
            graphicsDevice.Indices = ints;

            //set effect parameters
            effect.Parameters["ParticleTexture"].SetValue(texture);
            effect.Parameters["View"].SetValue(View);
            effect.Parameters["Projection"].SetValue(Projection);
            effect.Parameters["CurrentTime"].SetValue((float)gameTime.TotalGameTime.TotalMilliseconds);
            effect.Parameters["MaxTime"].SetValue(maxTime);
            effect.Parameters["Wind"].SetValue(wind);
            effect.Parameters["Size"].SetValue(particleSize / 2.0f);
            effect.Parameters["Up"].SetValue(Up);
            effect.Parameters["Side"].SetValue(Right);
            effect.Parameters["FadeInTime"].SetValue(fadeInTime);

         
            

            //enable blending render states
            graphicsDevice.BlendState = BlendState.AlphaBlend;
            graphicsDevice.DepthStencilState = DepthStencilState.DepthRead;

            //apply the effect
            effect.CurrentTechnique.Passes[0].Apply();

            //draw billboards
            graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, nParticles * 4, 0, nParticles * 2);

            //unset buffers
            graphicsDevice.SetVertexBuffer(null);
            graphicsDevice.Indices = null;

            //reset render states
            graphicsDevice.BlendState = BlendState.Opaque;
            graphicsDevice.DepthStencilState = DepthStencilState.Default;
 
        }

        //get a random vector between min and max
        private Vector3 RandVec(Vector3 min, Vector3 max)
        {
            return new Vector3(min.X + (float)rand.NextDouble() * (max.X - min.X),
                                min.Y + (float)rand.NextDouble() * (max.Y - min.Y),
                                min.Z + (float)rand.NextDouble() * (max.Z - min.Z));

        }

    }
}
